/*
 * Copyright 2009 Jamie Gennis
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.gwt.remoteaction.server;

import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.RETURN;
import static org.objectweb.asm.Opcodes.V1_5;

import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

import com.google.code.gwt.remoteaction.client.ActionError;
import com.google.code.gwt.remoteaction.client.ActionResult;
import com.google.code.gwt.remoteaction.client.ActionRuntimeException;

/**
 * A factory for ActionResult objects for a particular action method.  The
 * ActionResult class
 *
 * @author jgennis
 */
class ActionResultFactory {

  protected static class ActionResultClassLoader extends ClassLoader {

    protected final Class<?> actionResultClass;

    public ActionResultClassLoader(ClassLoader parent, String className, byte[] classBytes) {
      super(parent);
      this.actionResultClass = super.defineClass(className, classBytes, 0, classBytes.length);
    }

    public Class<?> getActionResultClass() {
      return actionResultClass;
    }
  }

  Class<? extends ActionResult> actionResultClass;

  Set<Class<? extends Throwable>> throwableClasses;

  Field resultField;

  Field thrownField;

  public ActionResultFactory(String className, Class<?> resultClass,
      Set<Class<? extends Throwable>> throwableClasses) {
    if (resultClass == null) {
      throw new IllegalArgumentException("The resultClass argument may not be null");
    }
    if (throwableClasses == null) {
      throw new IllegalArgumentException("The throwableClasses argument may not be null");
    }
    this.throwableClasses = new HashSet<Class<? extends Throwable>>(throwableClasses);
    // ActionRuntimeException and ActionError are always allowed
    this.throwableClasses.add(ActionRuntimeException.class);
    this.throwableClasses.add(ActionError.class);
    initActionResultClass(className, resultClass);
  }

  public ActionResult createSuccessResult(Object result) {
    Class<?> resultType = resultField.getType();
    if (result != null && !resultType.isAssignableFrom(result.getClass())) {
      throw new IllegalArgumentException("The result argument is not assignable to result type "
          + resultType.getCanonicalName());
    }
    ActionResult actionResult = createActionResult();
    try {
      resultField.set(actionResult, result);
    } catch (IllegalArgumentException ex) {
      throw new RuntimeException(ex);
    } catch (IllegalAccessException ex) {
      throw new RuntimeException(ex);
    }
    return actionResult;
  }

  public ActionResult createFailureResult(Throwable throwable) {
    ActionResult actionResult = createActionResult();
    if (!throwableClasses.contains(throwable.getClass())) {
      StringBuilder sb = new StringBuilder();
      sb.append("The throwable argument's type (" + throwable.getClass().getCanonicalName()
          + ") is not one of the allowed throwable types:");
      for (Class<? extends Throwable> throwableClass : throwableClasses) {
        sb.append("\n  " + throwableClass.getCanonicalName());
      }
      throw new IllegalArgumentException(sb.toString());
    }
    try {
      thrownField.set(actionResult, throwable);
    } catch (IllegalArgumentException ex) {
      throw new RuntimeException(ex);
    } catch (IllegalAccessException ex) {
      throw new RuntimeException(ex);
    }
    return actionResult;
  }

  protected ActionResult createActionResult() {
    try {
      return actionResultClass.newInstance();
    } catch (InstantiationException ex) {
      throw new RuntimeException(ex);
    } catch (IllegalAccessException ex) {
      throw new RuntimeException(ex);
    }
  }

  protected void initActionResultClass(String className, Class<?> resultType) {

    // Create the action class and extract the fields
    actionResultClass = createActionResultClass(className, resultType);
    try {
      resultField = actionResultClass.getField("result");
    } catch (NoSuchFieldException ex) {
      throw new RuntimeException(ex);
    } catch (SecurityException ex) {
      throw new RuntimeException(ex);
    }
    try {
      thrownField = actionResultClass.getField("thrown");
    } catch (NoSuchFieldException ex) {
      throw new RuntimeException(ex);
    } catch (SecurityException ex) {
      throw new RuntimeException(ex);
    }
  }

  @SuppressWarnings("unchecked")
  protected Class<? extends ActionResult> createActionResultClass(String className, Class<?> resultType) {
    if (resultType.isPrimitive())
      resultType = Utils.getReferenceType(resultType); // must not be a primitive type
    String resultTypeDesc = Type.getDescriptor(resultType);
    String throwableTypeDesc = Type.getDescriptor(Throwable.class);
    String internalClassName = className.replace('.', '/');
    ClassWriter cw = new ClassWriter(0);
    cw.visit(V1_5, ACC_PUBLIC, internalClassName, null, Type.getInternalName(Object.class),
        new String[]{Type.getInternalName(ActionResult.class)});
    // Fields
    cw.visitField(ACC_PUBLIC, "result", resultTypeDesc, null, null).visitEnd();
    cw.visitField(ACC_PUBLIC, "thrown", throwableTypeDesc, null, null).visitEnd();
    // Parameterless constructor
    MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
    mv.visitCode();
    mv.visitVarInsn(ALOAD, 0);
    mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V");
    mv.visitInsn(RETURN);
    mv.visitMaxs(2, 1);
    mv.visitEnd();
    // public Object getResult()
    mv = cw.visitMethod(ACC_PUBLIC, "getResult", "()" + Type.getDescriptor(Object.class),
        null, null);
    mv.visitCode();
    mv.visitVarInsn(ALOAD, 0);
    mv.visitFieldInsn(GETFIELD, internalClassName, "result", resultTypeDesc);
    mv.visitInsn(ARETURN);
    mv.visitMaxs(1, 1);
    mv.visitEnd();
    // public Throwable getException()
    mv = cw.visitMethod(ACC_PUBLIC, "getException", "()" + Type.getDescriptor(Throwable.class),
        null, null);
    mv.visitCode();
    mv.visitVarInsn(ALOAD, 0);
    mv.visitFieldInsn(GETFIELD, internalClassName, "thrown", throwableTypeDesc);
    mv.visitInsn(ARETURN);
    mv.visitMaxs(1, 1);
    mv.visitEnd();
    cw.visitEnd();

    ActionResultClassLoader classLoader = new ActionResultClassLoader(
        ActionResultClassLoader.class.getClassLoader(), className, cw.toByteArray());
    return (Class<? extends ActionResult>) classLoader.getActionResultClass();
  }

  protected static Class<?> defineClass(String className, byte[] classBytes) {
    Class<?> clazz = null;
    try {
      ClassLoader loader = ClassLoader.getSystemClassLoader();
      Class<?> cls = Class.forName("java.lang.ClassLoader");
      java.lang.reflect.Method method =
          cls.getDeclaredMethod("defineClass", new Class[]{String.class, byte[].class, int.class,
            int.class});

      // protected method invocaton
      method.setAccessible(true);
      try {
        Object[] args = new Object[]{
          className,
          classBytes,
          new Integer(0),
          new Integer(classBytes.length)
        };
        clazz = (Class<?>) method.invoke(loader, args);
      } finally {
        method.setAccessible(false);
      }
    } catch (Exception ex) {
      throw new RuntimeException(ex);
    }
    return clazz;
  }
}
